// Copyright 2019 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0

//! Events.

use ui_events::keyboard::{Code, Key, KeyState, KeyboardEvent};
use vello::kurbo::Rect;

use crate::dpi::PhysicalSize;
use crate::util::Duration;

// --- MARK: TYPES

/// A global event.
#[derive(Debug, Clone, PartialEq)]
pub enum WindowEvent {
    /// The window's DPI factor changed.
    Rescale(f64),
    /// The window was resized.
    Resize(PhysicalSize<u32>),
    /// The animation frame requested by this window must run.
    AnimFrame(Duration),
    /// The accessibility tree must be updated when rendering the app.
    EnableAccessTree,
    /// The accessibility tree is no longer updated when rendering the app.
    DisableAccessTree,
}

/// A text-related event.
#[derive(Debug, Clone, PartialEq)]
pub enum TextEvent {
    /// A keyboard event.
    Keyboard(KeyboardEvent),
    /// An IME event.
    Ime(Ime),
    /// The window took or lost focus.
    WindowFocusChange(bool),
    // TODO - Handle rich text copy-pasting
    /// The user pasted content in.
    ClipboardPaste(String),
}

/// An accessibility event.
///
/// This is generated by `accesskit` back-ends when the user interacts with
/// the app using their platform's accessibility APIs.
///
/// For instance, these events are often used by visually impaired users to navigate
/// your app without a mouse.
#[derive(Debug, Clone, PartialEq)]
pub struct AccessEvent {
    /// The action that was performed.
    pub action: accesskit::Action,
    /// Additional data associated with the action.
    pub data: Option<accesskit::ActionData>,
}

/// The light/dark mode of the window.
#[derive(Debug, Clone, PartialEq)]
pub enum WindowTheme {
    /// Light mode.
    Light,
    /// Dark mode.
    Dark,
}

/// Changes to widget state.
///
/// Unlike [`PointerEvent`](ui_events::pointer::PointerEvent)s,
/// [`TextEvent`]s and [`AccessEvent`]s, [`Update`] events
/// are generated by Masonry, and may occur at different times during a given pass of
/// the event loop.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum Update {
    /// Sent to a `Widget` when it is added to the widget tree. This should be
    /// the first message that each widget receives.
    ///
    /// Widgets should handle this event in order to do any initial setup that cannot
    /// be done when constructing the widget.
    WidgetAdded,

    /// Called when the [disabled] state of the widget is changed.
    ///
    /// See also [`is_disabled`] and [`set_disabled`].
    ///
    /// [disabled]: crate::doc::masonry_concepts#disabled
    /// [`is_disabled`]: crate::core::EventCtx::is_disabled
    /// [`set_disabled`]: crate::core::EventCtx::set_disabled
    DisabledChanged(bool),

    /// Called when the [stashed] state of the widget is changed.
    ///
    /// See also [`is_stashed`] and [`set_stashed`].
    ///
    /// [stashed]: crate::doc::masonry_concepts#stashed
    /// [`is_stashed`]: crate::core::EventCtx::is_stashed
    /// [`set_stashed`]: crate::core::EventCtx::set_stashed
    StashedChanged(bool),

    /// Called when a child widgets uses
    /// [`EventCtx::request_scroll_to_this`](crate::core::EventCtx::request_scroll_to_this).
    RequestPanToChild(Rect),

    /// Called when the [hovered] status of the current widget changes.
    ///
    /// [hovered]: crate::doc::masonry_concepts#widget-status
    HoveredChanged(bool),

    /// Called when the [hovered] status of the current widget or a descendant changes.
    ///
    /// This is sent before [`Update::HoveredChanged`].
    ///
    /// [hovered]: crate::doc::masonry_concepts#widget-status
    ChildHoveredChanged(bool),

    /// Called when the [active] status of the current widget changes.
    ///
    /// [active]: crate::doc::masonry_concepts#widget-status
    ActiveChanged(bool),

    /// Called when the [active] status of the current widget or a descendant changes.
    ///
    /// This is sent before [`Update::ActiveChanged`].
    ///
    /// [active]: crate::doc::masonry_concepts#widget-status
    ChildActiveChanged(bool),

    /// Called when the [focused] status of the current widget changes.
    ///
    /// [focused]: crate::doc::masonry_concepts#text-focus
    FocusChanged(bool),

    /// Called when the [focused] status of the current widget or a descendant changes.
    ///
    /// This is sent before [`Update::FocusChanged`].
    ///
    /// [focused]: crate::doc::masonry_concepts#text-focus
    ChildFocusChanged(bool),
}

/// An enum for specifying whether an event was handled.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Handled {
    /// An event was already handled, and shouldn't be propagated to other event handlers.
    Yes,
    /// An event has not yet been handled.
    No,
}

// --- MARK: IMPLS

impl TextEvent {
    /// Constructor for keyboard events with [`KeyState::Down`].
    ///
    /// The key code will be marked as [`Code::Unidentified`].
    pub fn key_down(key: Key) -> Self {
        Self::Keyboard(KeyboardEvent {
            state: KeyState::Down,
            key,
            code: Code::Unidentified,
            ..Default::default()
        })
    }

    /// Constructor for keyboard events with [`KeyState::Up`].
    ///
    /// The key code will be marked as [`Code::Unidentified`].
    pub fn key_up(key: Key) -> Self {
        Self::Keyboard(KeyboardEvent {
            state: KeyState::Up,
            key,
            code: Code::Unidentified,
            ..Default::default()
        })
    }

    /// Constructor for [`Ime::Preedit`] events.
    ///
    /// This is mostly useful for testing.
    pub fn preedit(text: String) -> Self {
        Self::Ime(Ime::Preedit(text, None))
    }

    /// Constructor for [`Ime::Preedit`] events.
    ///
    /// This is mostly useful for testing.
    ///
    /// **selected** is the part of the preedit text that should be selected.
    ///
    /// # Panics
    ///
    /// If **selected** isn't a substring of **text**.
    pub fn preedit_with_cursor(text: String, selected: String) -> Self {
        let Some(offset) = text.find(&selected) else {
            panic!("Error building Preedit event: '{selected}' not found in '{text}'");
        };
        let span = (offset, selected.len());
        Self::Ime(Ime::Preedit(text, Some(span)))
    }

    /// Short name of the event, for debug logging.
    pub fn short_name(&self) -> &'static str {
        match self {
            Self::Keyboard(KeyboardEvent { repeat: true, .. }) => "Keyboard(repeat)",
            Self::Keyboard(..) => "Keyboard",
            Self::Ime(Ime::Disabled) => "Ime::Disabled",
            Self::Ime(Ime::Enabled) => "Ime::Enabled",
            Self::Ime(Ime::Commit(_)) => "Ime::Commit",
            Self::Ime(Ime::Preedit(s, _)) if s.is_empty() => "Ime::Preedit(\"\")",
            Self::Ime(Ime::Preedit(_, _)) => "Ime::Preedit(\"...\")",
            Self::WindowFocusChange(_) => "WindowFocusChange",
            Self::ClipboardPaste(_) => "ClipboardPaste",
        }
    }
}

impl AccessEvent {
    /// Short name of the event, for debug logging.
    ///
    /// Returns the enum variant name.
    pub fn short_name(&self) -> &'static str {
        match self.action {
            accesskit::Action::Click => "Click",
            accesskit::Action::Focus => "Focus",
            accesskit::Action::Blur => "Blur",
            accesskit::Action::Collapse => "Collapse",
            accesskit::Action::Expand => "Expand",
            accesskit::Action::CustomAction => "CustomAction",
            accesskit::Action::Decrement => "Decrement",
            accesskit::Action::Increment => "Increment",
            accesskit::Action::HideTooltip => "HideTooltip",
            accesskit::Action::ShowTooltip => "ShowTooltip",
            accesskit::Action::ReplaceSelectedText => "ReplaceSelectedText",
            accesskit::Action::ScrollDown => "ScrollDown",
            accesskit::Action::ScrollLeft => "ScrollLeft",
            accesskit::Action::ScrollRight => "ScrollRight",
            accesskit::Action::ScrollUp => "ScrollUp",
            accesskit::Action::ScrollIntoView => "ScrollIntoView",
            accesskit::Action::ScrollToPoint => "ScrollToPoint",
            accesskit::Action::SetScrollOffset => "SetScrollOffset",
            accesskit::Action::SetTextSelection => "SetTextSelection",
            accesskit::Action::SetSequentialFocusNavigationStartingPoint => {
                "SetSequentialFocusNavigationStartingPoint"
            }
            accesskit::Action::SetValue => "SetValue",
            accesskit::Action::ShowContextMenu => "ShowContextMenu",
        }
    }
}

impl Update {
    /// Short name of the event, for debug logging.
    ///
    /// Returns the enum variant name.
    pub fn short_name(&self) -> &str {
        match self {
            Self::WidgetAdded => "WidgetAdded",
            Self::DisabledChanged(false) => "DisabledChanged(false)",
            Self::StashedChanged(false) => "StashedChanged(false)",
            Self::HoveredChanged(false) => "HoveredChanged(false)",
            Self::ChildHoveredChanged(false) => "ChildHoveredChanged(false)",
            Self::ActiveChanged(false) => "ActiveChanged(false)",
            Self::ChildActiveChanged(false) => "ChildActiveChanged(false)",
            Self::FocusChanged(false) => "FocusChanged(false)",
            Self::ChildFocusChanged(false) => "ChildFocusChanged(false)",
            Self::DisabledChanged(true) => "DisabledChanged(true)",
            Self::StashedChanged(true) => "StashedChanged(true)",
            Self::HoveredChanged(true) => "HoveredChanged(true)",
            Self::ChildHoveredChanged(true) => "ChildHoveredChanged(true)",
            Self::ActiveChanged(true) => "ActiveChanged(true)",
            Self::ChildActiveChanged(true) => "ChildActiveChanged(true)",
            Self::FocusChanged(true) => "FocusChanged(true)",
            Self::ChildFocusChanged(true) => "ChildFocusChanged(true)",
            Self::RequestPanToChild(_) => "RequestPanToChild(_)",
        }
    }
}

impl Handled {
    /// Has the event been handled yet?
    pub fn is_handled(self) -> bool {
        self == Self::Yes
    }
}

impl From<bool> for Handled {
    /// Returns `Handled::Yes` if `handled` is true, and `Handled::No` otherwise.
    fn from(handled: bool) -> Self {
        if handled { Self::Yes } else { Self::No }
    }
}

// --- MARK: WINIT EXPIES

/// Describes [input method](https://en.wikipedia.org/wiki/Input_method) events.
///
/// Mirrors [`winit::event::Ime`](https://docs.rs/winit/latest/x86_64-unknown-linux-gnu/winit/event/enum.Ime.html).
///
/// This is also called a "composition event".
///
/// Most keypresses using a latin-like keyboard layout simply generate a [`TextEvent::Keyboard`].
/// However, one couldn't possibly have a key for every single unicode character that the user
/// might want to type — so the solution operating systems employ is to allow the user to type these
/// using _a sequence of keypresses_ instead.
///
/// A prominent example of this is accents - many keyboard layouts allow you to first click the
/// "accent key", and then the character you want to apply the accent to. In this case, some
/// platforms will generate the following event sequence:
///
/// ```ignore
/// // Press "`" key
/// Ime::Preedit("`", Some((0, 0)))
/// // Press "E" key
/// Ime::Preedit("", None) // Synthetic event generated by winit to clear preedit.
/// Ime::Commit("é")
/// ```
///
/// Additionally, certain input devices are configured to display a candidate box that allow the
/// user to select the desired character interactively. (To properly position this box, you must use
/// [`RenderRootSignal::ImeMoved`](crate::app::RenderRootSignal::ImeMoved).)
///
/// An example of a keyboard layout which uses candidate boxes is pinyin. On a latin keyboard the
/// following event sequence could be obtained:
///
/// ```ignore
/// // Press "A" key
/// Ime::Preedit("a", Some((1, 1)))
/// // Press "B" key
/// Ime::Preedit("a b", Some((3, 3)))
/// // Press left arrow key
/// Ime::Preedit("a b", Some((1, 1)))
/// // Press space key
/// Ime::Preedit("啊b", Some((3, 3)))
/// // Press space key
/// Ime::Preedit("", None) // Synthetic event generated by winit to clear preedit.
/// Ime::Commit("啊不")
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
//#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Ime {
    /// Notifies when the IME was enabled.
    ///
    /// After getting this event you could receive [`Preedit`][Self::Preedit] and
    /// [`Commit`][Self::Commit] events. You should also start performing IME related requests
    /// like [`RenderRootSignal::ImeMoved`](crate::app::RenderRootSignal::ImeMoved).
    Enabled,

    /// Notifies when a new composing text should be set at the cursor position.
    ///
    /// The value represents a pair of the preedit string and the cursor begin position and end
    /// position. When it's `None`, the cursor should be hidden. When `String` is an empty string
    /// this indicates that preedit was cleared.
    ///
    /// The cursor position is byte-wise indexed.
    Preedit(String, Option<(usize, usize)>),

    /// Notifies when text should be inserted into the editor widget.
    ///
    /// Right before this event winit will send empty [`Self::Preedit`] event.
    Commit(String),

    /// Notifies when the IME was disabled.
    ///
    /// After receiving this event you won't get any more [`Preedit`][Self::Preedit] or
    /// [`Commit`][Self::Commit] events until the next [`Enabled`][Self::Enabled] event.
    Disabled,
}

/// Defines the orientation that a window resize will be performed.
///
/// Mirrors [`winit::window::ResizeDirection`](https://docs.rs/winit/latest/x86_64-unknown-linux-gnu/winit/window/enum.ResizeDirection.html).
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[expect(missing_docs, reason = "Copied from winit")]
pub enum ResizeDirection {
    East,
    North,
    NorthEast,
    NorthWest,
    South,
    SouthEast,
    SouthWest,
    West,
}
